<?php
/**
 * This file is part of the Sphoof framework.
 * Copyright (c) 2010-2011 Sphoof
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code. You can also view the
 * LICENSE file online at http://www.sphoof.nl/new-bsd.txt
 *
 * @category	Sphoof
 * @copyright	Copyright (c) 2010-2011 Sphoof (http://sphoof.nl)
 * @license		http://sphoof.nl/new-bsd.txt	New BSD License
 * @package		View
 */

require_once( realpath( dirname( __FILE__ ) ) . '/exception.php' );
require_once( realpath( dirname( __FILE__ ) ) . '/sphoof.php' );

/**
 * An Exception that will be thrown when you try to include a template that can
 * not be found.
 *
 * @package		View
 * @subpackage	Exception
 */
class SpNonExistentTemplate extends SpException { }

/**
 * An exception that will be thrown when you try to close a slot that hasn't
 * been started yet.
 *
 * @package		View
 * @subpackage	Exception
 */
class SpSlotNotStarted extends SpException { }

/**
 * An Exception that will be thrown when you try to call a helper that doesn't
 * exist.
 *
 * @package		View
 * @subpackage	Exception
 */
class SpHelperNotRegistered extends SpException { }

/**
 * An Exception that will be thrown when you try to call the content method
 * while the current template is not being used as decoration.
 *
 * @package		View
 * @subpackage	Exception
 */
class SpNotInDecorationContext extends SpException { }

/**
 * The registry will contain all instances that should be shared between
 * multiple views, such as helpers and slots.
 */
class SpViewRegistry {
	/**
	 * An array of alias to classname mappings.
	 *
	 * @var Array
	 */
	protected $helpers = array(
		'script' => 'SpScriptHelper',
		'title' => 'SpTitleHelper',
		'style' => 'SpStyleHelper'
	);

	/**
	 * An array of classname => instance mappings.
	 *
	 * @var Array
	 */
	protected $helperInstances;

	/**
	 * An array of name => SpSlot mappings.
	 *
	 * @var Array
	 */
	protected $slots;

	/**
	 * Register a helper with the alias $alias.
	 *
	 * @param string $classname
	 * @param string $alias
	 */
	public function attachHelper( $object, $alias ) {
		if( is_object( $object ) && $class = get_class( $object ) ) {
			$this->helperInstances[$class] = $object;
			$this->helpers[$alias] = $class;
			return;
		}
		$this->helpers[$alias] = $object;
	}

	/**
	 * Returns the helper with the alias $alias.
	 *
	 * @param string $alias
	 * @return SpHelper
	 */
	public function helper( $alias ) {
		if( isset( $this->helpers[$alias] ) ) {
			return $this->create( $this->helpers[$alias] );
		}
		throw new SpHelperNotRegistered( sprintf( 'No helper registered with the alias "%s"', $alias ) );
	}

	/**
	 * Returns a slot with the name $slot. If the slot does not exist, it will
	 * be created and returned.
	 *
	 * @param string $slot
	 * @return SpSlot
	 */
	public function slot( $slot ) {
		if( !isset( $this->slots[$slot] ) ) {
			$this->slots[$slot] = new SpSlot( $slot );
		}
		return $this->slots[$slot];
	}

	/**
	 * Creates and returns the helper with classname $classname. If it was
	 * already created, the older instance will be returned.
	 *
	 * @param string $classname
	 * @return SpViewHelper
	 */
	public function create( $classname ) {
		if( !isset( $this->helperInstances[$classname] ) ) {
			$this->helperInstances[$classname] = new $classname;
		}
		return $this->helperInstances[$classname];
	}
}

abstract class SpViewRenderer {

	/**
	 * Constains a reference to the SpViewRegistry.
	 *
	 * @var SpViewRegistry
	 */
	protected $registry;

	/**
	 * An array of parameters that the template can access.
	 *
	 * @var Array
	 */
	protected $parameters = array( );

	/**
	 * A template that will be used to decorate this view.
	 *
	 * @var String
	 */
	protected $decoration;

	/**
	 * An array of parameters that will be passed to the decorator.
	 *
	 * @var Array
	 */
	protected $decoratorParameters;

	/**
	 * Path to the templates
	 *
	 * @var String
	 */
	protected $path;

	/**
	 * Construct the view renderer.
	 *
	 * @param SpViewRegistry $registry
	 */
	public function __construct( SpViewRegistry $registry = null ) {
		$this->registry = $registry;
	}

	public function __set( $name, $value ) {
		return $this->parameters[$name] = $value;
	}

	/**
	 * Returns the View registry. If no registry was passed, it will be created.
	 *
	 * @return SpViewRegistry
	 */
	public function registry( ) {
		if( null === $this->registry ) {
			$this->registry = new SpViewRegistry( );
		}
		return $this->registry;
	}

	/**
	 * Attach a helper of $classname to the viewregistry and register it by $alias.
	 *
	 * @param string $classname
	 * @param string $alias
	 * @return SpViewRenderer
	 */
	public function attachHelper( $classname, $alias ) {
		return $this->registry( )->attachHelper( $classname, $alias );
	}

	/**
	 * Finds and returns the helper object associated with the alias $alias.
	 *
	 * @param string $helper
	 * @return SpHelper
	 */
	public function helper( $alias ) {
		return $this->registry( )->helper( $alias );
	}

	/**
	 * Returns the slot with name $slot. If the slot doesn't exist, a new slot is created.
	 *
	 * @param string $slot
	 * @return SpSlot
	 */
	public function slot( $slot ) {
		return $this->registry( )->slot( $slot );
	}

	/**
	 * A method that will do the actual rendering of the template, after sanity
	 * checks. This method must be implemented by the concrete implementations.
	 *
	 * @param string $template
	 * @return string
	 */
	abstract protected function template( $template, Array $parameters );

	/**
	 * Renders the template with filename $template and returns its rendered content.
	 *
	 * @param string $template
	 * @return string
	 */
	public function render( $template, Array $parameters = array( ) ) {
		$content = $this->template( $this->file( $template ), $this->parameters( $parameters ) );
		if( null !== $this->decoration ) {
			$content = $this->decorator( )->decorateContent( $content )->withTemplate( $this->decoration );
			$this->decorate( null ); /** Only decorate once. */
		}
		return $content;
	}

	/**
	 * Tries to find the template file on the given path and return the name.
	 * Throws an exception if it can't be found.
	 *
	 * @param string $template
	 * @return string
	 */
	protected function file( $template ) {
		$template = ( isset( $this->path ) ? ( $this->path . $template ) : $template );
		if( false === sphoof_file_exists( $template ) ) {
			throw new SpNonExistentTemplate( sprintf( 'Template file %s could not be found.', $template ) );
		}
		return $template;
	}

	/**
	 * Sets the default path to look for templates.
	 *
	 * @param string $path
	 * @return SpView
	 */
	public function path( $path ) {
		$this->path = rtrim( $path, '/' ) . '/';

		$this->decorator( )->path( $path );

		return $this;
	}

	/**
	 * Returns a decoration object. This should be implemented by the
	 * concrete classes.
	 *
	 * @return SpPhpDecorator
	 */
	abstract protected function decorator( );

	/**
	 * Decorates the view with the template $template.
	 *
	 * @param string $template
	 * @return void
	 */
	public function decorate( $template, Array $parameters = null ) {
		$this->decoration = $template;
		$this->decoratorParameters = $parameters;
	}

	/**
	 * Parses a template and returns the contents. The partial has access to the
	 * parameters of this view and the ones that are passed, the ones that are
	 * being passed take precedence.
	 *
	 * @param string $template
	 * @param array $parameters
	 * @return string
	 */
	public function partial( $template, array $parameters = array( ) ) {
		return $this->renderer( )->render(
			$this->file( $template ),
			array_merge( $this->parameters, $parameters )
		);
	}

	/**
	 * Creates a new instance of self, and returns it.
	 *
	 * @return SpViewRenderer
	 */
	protected function renderer( ) {
		$classname = get_class( $this );
		return new $classname( $this->registry( ) );
	}

	/**
	 * Overwriting PHP's way of retrieving parameters, so we have the ability to
	 * filter all values before parsing.
	 *
	 * @param string $name
	 * @return mixed
	 */
	public function __get( $name ) {
		if( isset( $this->parameters[$name] ) ) {
			return $this->parameters[$name];
		}
		return ( true == trigger_error( sprintf( 'Undefined property "%s"', $name ) ) ) ? false : null;
	}

	/**
	 * Overwriting PHP's way of checking existence of parameters.
	 *
	 * @param string $name
	 * @return mixed
	 */
	public function  __isset( $name ) {
		return isset( $this->parameters[$name] );
	}

	/**
	 * Takes an array of parameters, sets them on the current object and returns
	 * the merged result.
	 *
	 * @param array $parameters
	 * @return Array
	 */
	protected function parameters( array $parameters ) {
		return (array) ( $this->parameters = array_merge( $this->parameters, $parameters ) );
	}

	/**
	 * A convenience method for alerting the user that the current template is
	 * not used as a decoration.
	 */
	protected function content( ) {
		throw new SpNotInDecorationContext( 'Can not call content: the current template is not used as a decoration.' );
	}
}

class SpPhpRenderer extends SpViewRenderer {
	/**
	 * Used decorator
	 *
	 * @var SpPhpDecorator
	 */
	protected $decorator;

	/**
	 * Renders a template and returns the result.
	 *
	 * @param string $template
	 * @param array $parameters
	 * @return string
	 */
	protected function template( $template, Array $parameters ) {
		ob_start( );
		include( $template );
		return ob_get_clean( );
	}

	/**
	 * Returns a new instance of the decorator.
	 *
	 * @return SpPhpDecoration
	 */
	protected function decorator( ) {
		if( !isset( $this->decorator ) ) {
			$this->decorator = new SpPhpDecorator( $this->registry( ) );
			$this->decorator->parameters( (array) $this->decoratorParameters );
		}

		return $this->decorator;
	}
}

class SpPhpDecorator extends SpPhpRenderer {

	/**
	 * Contains the content of the inner template.
	 *
	 * @var String
	 */
	protected $inner;

	/**
	 * Sets the default path to look for templates.
	 *
	 * @param string $path
	 * @return SpView
	 */
	public function path( $path ) {
		$this->path = rtrim( $path, '/' ) . '/';

		return $this;
	}

	/**
	 * Tells the decorator which content to decorate.
	 *
	 * @param string $content
	 * @return SpPhpDecorator
	 */
	public function decorateContent( $content ) {
		$this->inner = $content;
		return $this;
	}

	/**
	 * Renders the decorative-template and returns the result.
	 *
	 * @param string $template
	 * @return string
	 */
	public function withTemplate( $template ) {
		return $this->render( $template, array( ) );
	}

	/**
	 * Convenience method which will return the content of the inner template
	 * for use in the decorative template.
	 *
	 * @return string
	 */
	protected function content( ) {
		return (string) $this->inner;
	}
}

/**
 * A slot is an implementation of placeholders you can use in any of the
 * involved view elements, such as partials, templates or the layout. By filling
 * the placeholder you can use the contents in each of the other elements.
 */
class SpSlot {

	/**
	 * The name associated with the slot.
	 * @var string
	 */
	protected $name;

	/**
	 * Indicates whether buffering has started.
	 *
	 * @var boolean
	 */
	protected $buffering;

	/**
	 * The output of the slot.
	 * @var string
	 */
	protected $output;

	/**
	 * Constructs the slot, associating the name with the instance.
	 * @param string $name
	 */
	public function __construct( $name ) {
		$this->name = $name;
	}

	/**
	 * Returns the contents of the slot, or false if it hasn't been filled.
	 *
	 * @return mixed
	 */
	public function render( ) {
		return ( null !== $this->output ) ? $this->output : false;
	}

	/**
	 * Outputs the contents of the slot and returns true, or returns false if
	 * the slot hasn't been filled.
	 *
	 * @return boolean
	 */
	public function output( ) {
		if( null !== $this->output ) {
			echo $this->output;
			return true;
		}
		return false;
	}

	/**
	 * Start buffering the content of the slot.
	 *
	 * @return boolean
	 */
	public function start( ) {
		return ( $this->buffering = ob_start( ) );
	}

	/**
	 * Finish buffering the content of the slot.
	 *
	 * @return boolean
	 */
	public function end( ) {
		if( true !== $this->buffering && ob_get_level( ) == $this->buffering ) {
			throw new SpSlotNotStarted( sprintf( 'Could not end slot "%s", slot was never started.', $this->name ) );
		}
		$this->output = ob_get_clean( );
	}
}

/**
 * A default helper that can be used to collect which scripts to link to.
 */
class SpScriptHelper {

	/**
	 * An array of scripts that should be included.
	 *
	 * @var Array
	 */
	protected $scripts = array( );

	/**
	 * Add a script to the queue. If you're adding the same script twice, it
	 * will be displayed only once.
	 *
	 * @param string $script
	 * @return SpScriptHelper
	 */
	public function add( $script ) {
		if( !in_array( $script, $this->scripts ) ) {
			$this->scripts[] = $script;
		}
		return $this;
	}

	/**
	 * Returns the array of scripts, which might be useful if you want to render
	 * the script tags yourself.
	 *
	 * @return Array
	 */
	public function scripts( ) {
		return $this->scripts;
	}

	/**
	 * Returns the queue of scripts, wrapped in appropriate script tags.
	 *
	 * @return String
	 */
	public function __toString( ) {
		return (string) $this->output( );
	}

	/**
	 * Returns the queue of scripts, wrapped in appropriate script tags.
	 *
	 * @return string
	 */
	protected function output( ) {
		return implode( PHP_EOL, array_map( array( $this, 'wrap' ), $this->scripts ) );
	}

	/**
	 * Wraps the script name in the appropriate HTML and returns it.
	 *
	 * @param string $script
	 * @return string
	 */
	protected function wrap( $script ) {
		return sprintf( "<script type=\"text/javascript\" src=\"%s\"></script>", $script );
	}
}

/**
 * A default helper that can be used to collect which CSS style sheets to link to.
 */
class SpStyleHelper {
	/**
	 * An array of css files that should be included.
	 *
	 * @var Array
	 */
	protected $styles = array( );

	/**
	 * Add a css file to the queue. If you're adding the same css file twice, it
	 * will be displayed only once.
	 *
	 * @param string $cssfile
	 * @return SpStyleHelper
	 */
	public function add( $style ) {
		if( !in_array( $style, $this->styles ) ) {
			$this->styles[] = $style;
		}
		return $this;
	}

	/**
	 * Returns the array of scripts, which might be useful if you want to render
	 * the script tags yourself.
	 *
	 * @return Array
	 */
	public function styles( ) {
		return array_reverse( $this->styles );
	}

	/**
	 * Returns the queue of scripts, wrapped in appropriate script tags.
	 *
	 * @return String
	 */
	public function __toString( ) {
		return (string) $this->output( );
	}

	/**
	 * Returns the queue of scripts, wrapped in appropriate script tags.
	 *
	 * @return string
	 */
	protected function output( ) {
		return implode( PHP_EOL, array_map( array( $this, 'wrap' ), $this->styles ) );
	}

	/**
	 * Wraps the stylesheets' name in appropriate HTML and returns it.
	 *
	 * @param string $style
	 * @return string
	 */
	protected function wrap( $style ) {
		return sprintf( '<link href="%s" rel="stylesheet" type="text/css" />', $style );
	}
}

/**
 * Utility helper to determine which title to display.
 */
class SpTitleHelper {

	/**
	 * Contains the title if it is set by one of the inner templates.
	 * @var string
	 */
	protected $title;

	/**
	 * Returns the title if it was set previously, or the value of $default if it wasn't.
	 *
	 * @param string $default
	 * @return string
	 */
	public function title( $default ) {
		return isset( $this->title ) ? $this->title : $default;
	}

	/**
	 * Sets the title for the template.
	 *
	 * @param String $title
	 */
	public function set( $title ) {
		$this->title = $title;
	}
}